import copy
import ctypes
import inspect
import hashlib
import logging
import json
import re
import time
import ctypes
import uuid
import os
from functools import reduce, wraps
from ipaddress import IPv4Address
from typing import List, Union
import threading

import openstack
from faker import Faker
from flask import current_app, request, copy_current_request_context, g
from flask_restx import abort
from sqlalchemy import or_

from app.common.libs import auth
from app.models import ctfModel, rangeModel, switchModel, userModel
from app.models.ctfModel import CtfRedBlue
from configs.settings import flask_config
from tools import db
from tools import redis_conn as redis_store
from tools.basic_tools import Tools
from utils.crypto import Crypt, decrypt_pw

logger = logging.getLogger(__name__)


def init_openstack(project_id, config):
    """
    根据客户定制配置虚拟化参数
    """
    openstackAuthConfig = decrypt_pw(config["OPENSTACK"]["auth"])
    openstackAuthConfig["project_id"] = project_id

    try:
        openstack_conn = openstack.connect(
            auth=openstackAuthConfig, auth_type=config["OPENSTACK"]["auth_type"]
        )
    except Exception as e:
        current_app.logger.exception("初始化openstack连接失败", exc_info=e)
        abort(500, message="无法连接到云计算服务")
    else:
        return openstack_conn


def init_opserver_conn():
    info = db.session.query(userModel.User).filter(userModel.User.id == 1).first()
    op_conn = init_openstack(info.project_id, current_app.config)
    return op_conn


def list_dict_duplicate_removal(data_list):
    def run_function(x, y):
        return x if y in x else x + [y]

    return reduce(
        run_function,
        [
            [],
        ]
        + data_list,
    )


def base_detail(info, paginate):
    info["per_page"] = paginate.per_page
    info["total"] = paginate.total
    info["pages"] = paginate.pages
    info["prev_num"] = paginate.prev_num
    info["next_num"] = paginate.next_num
    info["has_prev"] = paginate.has_prev
    info["has_next"] = paginate.has_next
    return info


def create_md5(s):
    salt = Faker().pystr(min_chars=None, max_chars=16)
    s = str(s + salt).encode()
    m = hashlib.md5(s)
    return m.hexdigest()


def checkCourseRange(range_uuid=None, courseware_id=None):
    """
    检测课件ID，是否关联章节id，chapter_id:章节ID ；courseware_id：课件ID
    """
    if range_uuid:
        sql_str = f"select * from api_courseware where range_uuid='{range_uuid}'"
    elif courseware_id:
        sql_str = f"select * from api_courseware where id='{courseware_id}'"
    else:
        return

    # 检测课件是否关联此靶场id
    courseware_info = db.session.execute(sql_str).fetchone()
    if not courseware_info:
        return

    course_info = db.session.execute(
        f"select * from api_chapter_courseware as a "
        f"left join api_chapter as b on a.chapter_id=b.id "
        f"where a.courseware_id={courseware_info['id']}"
    ).fetchone()
    if course_info:
        abort(400, message="此靶场已经关联了课程，不能进行修改！")

    return


def checkCtfRange(range_uuid):
    """
    根据range_uuid 判断ctf比赛开启状态,如果开启将不能进行操作
    """
    ctfInfo = ctfModel.Ctf.query.filter_by(range_uuid=range_uuid).first()

    if not ctfInfo:
        return

    if ctfInfo.status == 1:
        abort(400, message="正在开启的比赛，不能进行修改！")
    return


def checkCtfRangeStatus(range_uuid):
    """
    根据range_uuid 判断ctf比赛开启状态,比赛暂未开始或已经结束将不能进行操作
    """
    ctfInfo = ctfModel.Ctf.query.filter_by(range_uuid=range_uuid).first()

    if not ctfInfo:
        abort(400, message="暂无比赛！")

    if ctfInfo.status != 1:
        abort(400, message="比赛暂未开始或已经结束，不能开启环境！")
    return


def checkCtfQuestion(ctf_question_id):
    linkInfo = ctfModel.CtfLinkQuestion.query.filter_by(
        ctf_question_id=ctf_question_id
    ).first()

    if linkInfo:
        abort(400, message="比赛和试题已经关联，不能进行修改！")
    return


def update_uuid(topology_json):
    range_vm_type = flask_config.RANGE_CONF["RangeVmType"]
    topology_json = json.loads(topology_json)
    vm_uuid_dict = {}
    if "nodeList" in topology_json:
        for node in topology_json["nodeList"]:
            if (
                "type" in node
                and node["type"] in range_vm_type.values()
                and "vm_uuid" in node
            ):
                vm_uuid = str(uuid.uuid1())
                node["vm_uuid"] = vm_uuid
                node["vm_port_ip"] = "true"
                vm_uuid_dict[node["name"]] = {
                    "vm_uuid": vm_uuid,
                    "port_id": Tools.get_uuid4_no_line()
                }
            if "state" in node and len(node["state"]) > 0:
                node["state"] = ""
            if node["type"] == "router":
                router_id = Tools.get_uuid4_no_line()
                vm_uuid_dict[node["router_id"]] = router_id
                node["router_id"] = router_id
    print(vm_uuid_dict)
    return str(topology_json).replace("'", '"'), vm_uuid_dict


def update_json_router_switch(topology_json, range_uuid):
    switch_list = switchModel.Switchs.query.filter_by(range_uuid=range_uuid).all()
    port_lsit = switchModel.Ports.query.filter_by(
        range_uuid=range_uuid, device_owner="router_gw"
    ).all()
    switch_dict = {switch.cidr: switch.subnet_id for switch in switch_list}
    router_dict = {port.ip: port.device_id for port in port_lsit}
    topology_json = json.loads(topology_json)
    nodeList = topology_json.get("nodeList", "")
    for node in nodeList:
        if node.get("type") == "switch":
            node["subnet_id"] = switch_dict[node["cidr"]]
        elif node.get("type") == "router":
            node["router_id"] = router_dict[node["gw_ip"][0]["ip"]]
    return str(topology_json).replace("'", '"')


def judge_topo(topology_json):
    if "nodeList" in topology_json:
        range_vm_type = flask_config.RANGE_CONF["RangeVmType"]
        for node in topology_json["nodeList"]:
            if (
                "type" in node
                and node["type"] in range_vm_type.values()
                and "state" in node
                and node["state"].lower()
                not in ["created", "poweron", "poweroff", "build"]
            ):
                return node["name"]
    return None


def update_rule_body(args):
    protocol_dict = {
        "DNS": 53,
        "HTTP": 80,
        "HTTPS": 443,
        "IMAP": 143,
        "IMAPS": 993,
        "LDAP": 389,
        "MS SQL": 1433,
        "MYSQL": 3306,
        "POP3": 110,
        "POP3S": 995,
        "RDP": 3389,
        "SMTP": 25,
        "SMTPS": 465,
        "SSH": 22,
    }
    protocol = args.protocol
    port_range_min, port_range_max = args.port_range_min, args.port_range_max
    if protocol_dict.get(protocol):
        port_range_min = port_range_max = protocol_dict[protocol]
        protocol = "TCP"
    elif protocol == "ALL":
        port_range_min = port_range_max = -2
    elif port_range_min == port_range_max == "":
        port_range_min = port_range_max = -1
    return protocol, port_range_min, port_range_max


def get_task_status(range_uuid, tid):
    from celery.result import AsyncResult
    from tools import celery_app

    coon = celery_app.channel_client
    # coon = Redis(
    #     host=redis_config.get("host"),
    #     port=redis_config.get("port"),
    #     password=Crypt().decrypt(redis_config.get("password")),
    #     db=3,
    # )

    redis_tid = coon.get(range_uuid)
    coon.set(range_uuid, tid)
    if not redis_tid:
        return

    from tools import celery_app

    # or celery_app.AsyncResult
    async_task = AsyncResult(id=redis_tid.decode(), app=celery_app)
    if async_task.state == "PENDING":
        async_task.revoke()
    return


def get_tow_hours_stamp(course_id):
    redis_res = redis_store.get(course_id)
    ctime = time.time()
    if redis_res:
        return redis_res.decode()
    #utc_time = int((ctime + 60 * 60 * 24) * 1000)
    utc_time = int((ctime + 60 * 20) * 1000)
#    redis_store.set(course_id, utc_time, 60 * 60 * 24)
    redis_store.set(course_id, utc_time, 60 * 20)
    return utc_time


def check_ip(ip):
    if re.match(
        r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$",
        ip,
    ):
        return ip
    raise ValueError(f"The parameter '{ip}' is not correct")


def check_port(port):
    if isinstance(port, int) and 0 <= port <= 65535:
        return port
    raise ValueError(f"The parameter '{port}' is not correct")


def is_Chinese(word):
    for ch in word:
        if "\u4e00" <= ch <= "\u9fff":
            raise ValueError("不能包含汉字")
    return word


def get_ip(current_request):
    if current_request.access_route:
        return current_request.access_route[0]
    else:
        return current_request.remote_addr or "0.0.0.0"


def counter_time(func):
    """
    装饰器：记录函数调用次数, 耗时
    """
    counter = 0

    @wraps(func)
    def decorated(*args, **kwargs):
        nonlocal counter
        st = time.perf_counter()
        counter += 1
        ret = func(*args, **kwargs)
        print(
            f"Function: {func.__name__}, "
            f"Counter: {counter}, "
            f"time cost: {time.perf_counter() - st} seconds"
        )
        return ret

    return decorated


def get_conversion_ip(ip: Union[int, str] = None):
    if not ip:
        ip = get_ip(request)
    if isinstance(ip, str):
        return int(IPv4Address(ip))
    elif isinstance(ip, int):
        return str(IPv4Address(ip))
    else:
        return ip


def get_link_range_model() -> List[str]:
    """获取已被关联比赛或课程的靶场range_uuid"""
    unlink_range_model = db.session.execute(
        """
        SELECT api_ctf.range_uuid FROM api_ctf
        WHERE api_ctf.range_uuid IS NOT NULL
        UNION SELECT api_ctf_red_blue.range_uuid FROM api_ctf_red_blue
        WHERE api_ctf_red_blue.range_uuid IS NOT NULL
        UNION SELECT api_courseware.range_uuid FROM api_courseware
        WHERE api_courseware.range_uuid IS NOT NULL
    """
    )
    return list(unlink_range_model.scalars())


def get_link_range_id() -> List[int]:
    """获取已被关联比赛或课程的靶场id"""
    unlink_range_uuids = get_link_range_model()
    if not unlink_range_uuids:
        return []
    range_ids = (
        db.session.query(rangeModel.RangeLinkModule.range_id)
        .filter(rangeModel.RangeLinkModule.range_uuid.in_(unlink_range_uuids))
        .all()
    )
    return [val[0] for val in range_ids]


def range_expand_filter(range_uuid, rang_id=None):
    """判断当前用户是否作为该场景关联比赛的导调/裁判角色"""
    user = auth.current_user()
    if range_uuid:
        ctf_rb_filter = CtfRedBlue.query.filter(
            CtfRedBlue.range_uuid == range_uuid,
            or_(CtfRedBlue.rb_pilot_id == user.id, CtfRedBlue.rb_referee_id == user.id),
        )
    else:
        range_link = rangeModel.RangeLinkModule.query.filter_by(
            range_id=rang_id
        ).first()
        if not range_link:
            return False
        ctf_rb_filter = CtfRedBlue.query.filter(
            CtfRedBlue.range_uuid == range_link.range_uuid,
            or_(CtfRedBlue.rb_pilot_id == user.id, CtfRedBlue.rb_referee_id == user.id),
        )
    return db.exists(ctf_rb_filter)


def getMyRangeByUUID(range_uuid):
    _filter = {"range_uuid": range_uuid}
    user = auth.current_user()

    if not any([user.is_superuser, range_expand_filter(range_uuid)]):
        _filter["user_id"] = user.id

    range_link_module_info = rangeModel.RangeLinkModule.query.filter_by(
        **_filter
    ).first()
    if not range_link_module_info:
        abort(400, message="靶场不存在")
    return range_link_module_info


class CreateImageError(Exception):
    """取消Celery任务失败"""


def revoke_task(task_ids: list):
    """取消Celery任务"""
    from tools import celery_app

    for item in task_ids:
        value = redis_store.get(item)
        if value:
            try:
                celery_app.control.revoke(value.decode())
            except CreateImageError as e:
                current_app.logger.exception("取消Celery任务失败", exc_info=e)


def get_current_app_context():
    try:
        from flask.globals import _cv_app
        return _cv_app.get(None)
    except ImportError:
        from flask.globals import _app_ctx_stack
        return _app_ctx_stack.top


def push_app_context(fn):
    app = current_app._get_current_object()
    _g = copy.copy(g)

    def wrapper(*args, **kwargs):
        with app.app_context():
            ctx = get_current_app_context()
            ctx.g = _g
            return fn(*args, **kwargs)

    return wrapper


def decoration_context(func):
    """返回一个上下文包装的方法"""
    return push_app_context(copy_current_request_context(func))


def _async_raise(tid, exception):
    """raises the exception, performs cleanup if needed"""
    tid = ctypes.c_long(tid)
    if not inspect.isclass(exception):
        exception = type(exception)
    res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exception))
    if res == 0:
        logger.warning(f"线程: {tid} 不存在")
        return True
    elif res == 1:
        return True
    else:
        # 不为1时必定为异常状态，传递None恢复
        ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None)
        logger.error(f"线程: {tid} 强制退出失败")
        return False


def stop_thread(thread_id: int):
    from threading import enumerate as en_threads

    for val in en_threads():
        if val.ident == thread_id:
            try:
                # 尝试线程是否实现了stop方法
                val.stop()
            except AttributeError:
                logger.error(f"线程{thread_id}不存在自定义stop方法，进行强制停止")
                _async_raise(thread_id, SystemExit)
            break
    else:
        _async_raise(thread_id, SystemExit)


class ThreadTimeOutException(Exception):
    pass


def time_out(timeout):
    def decorator(func):
        def wrapper(*args, _auth=None, _role=None, throw_exception=True, **kwargs):

            result = []

            def save_result(f, f_args, f_kwargs, auth_user=None):
                if auth_user:
                    g.flask_httpauth_user = auth_user
                result.append(f(*f_args, **f_kwargs))

            try:
                _auth = _auth or g.flask_httpauth_user
            except:  # noqa
                _auth = None
            param_info = {
                "f": func,
                "f_args": args,
                "f_kwargs": kwargs,
                "auth_user": _auth
            }
            target_func = push_app_context(save_result)

            t = threading.Thread(target=target_func, args=(), kwargs=param_info)
            t.setDaemon(True)
            t.start()
            t.join(timeout=timeout)

            if t.is_alive():
                stop_thread(t.ident)
                if throw_exception:
                    raise ThreadTimeOutException
                return
            else:
                return result[0] if result else None

        return wrapper

    return decorator

def wait_for_container_status(
        zun_conn, container_ident, status="Created", timeout=60, _auth=None, throw_exception=True
):
    def _wait(conn, container_id, _status="Created"):
        while True:
            container = conn.containers.get(container_id)
            if container.status == _status:
                return container
            time.sleep(1)

    return time_out(timeout)(_wait)(
        zun_conn, container_ident, _status=status, _auth=_auth, throw_exception=throw_exception
    )
